--[[
title: 日志
description: 日志及调试相关函数
]]

-- 全局函数
local _G_load = _G.load;
local _G_xpcall = _G.xpcall;

local function debugstack(depth, message)
    message = (string.match(message or "", ":%d+:(.*)$")) or message;

    depth = depth or 1;
    local text = "";
    while (true) do
        local info = debug.getinfo(depth, "nSl");
        if (not info or info == "") then break end
        if (info.what == "C") then
            text = text .. "C function: " .. (info.name or "") .. "\n";
        else
            filepath = string.sub(info.source, 1, string.find(info.source, "\n", 1, true));
            filepath = string.gsub(filepath, "%s*$", "");
            filepath = string.gsub(filepath, "^%s*", "");
            if (text == "") then
                if (message ~= "") then
                    text = string.format("\n%s:%d %s\nstack traceback:\n", filepath, info.currentline, message)
                else
                    text = "\nstack traceback:\n";
                end
            end
            text = text .. (string.format("%s:%d %s\n", filepath, info.currentline, info.name or ""));
        end
        depth = depth + 1;
    end
    return text;
end

local function GetLocationInfo(level)
    local info = debug.getinfo(level or 1, "nSl")
    if not info then return "" end
    if info.what == "C" then return "C function" end
    local filepath = info.source;
    filepath = string.sub(filepath, 1, string.find(info.source, "\n", 1, true));
    filepath = string.gsub(filepath, "%s*$", "");
    filepath = string.gsub(filepath, "^%s*", "");
    return string.format("%s:%d %s", filepath, info.currentline, info.name or "");
end

local function ToString(val, key, indent, output_table, text)
    indent = indent or "";
    output_table = output_table or {};
    text = text and (text .. "\n") or "";

    if (type(val) ~= "table" or output_table[val]) then
        if (key and key ~= "") then
            text = text .. string.format("%s%s = %s", indent, tostring(key), tostring(val));
        else
            text = text .. string.format("%s%s", indent, tostring(val));
        end
        return text;
    end

    if (key and key ~= "") then
        text = text .. string.format("%s%s = %s {", indent, key, tostring(val));
    else
        text = text .. string.format("%s%s {", indent, tostring(val));
    end

    output_table[val] = true; -- 表记已输出

    for k, v in pairs(val) do text = text .. "\n" .. ToString(v, k, indent .. "    ", output_table); end

    text = text .. "\n" .. string.format("%s}", indent);

    return text;
end

local function Print(output, name, depth, level, ...)
    local args = table.pack(...);
    for i = 1, args.n do args[i] = ToString(args[i]) end
    local filepos = GetLocationInfo(depth or 3) or "";
    local datetime = os.date("%Y-%m-%d %H:%M:%S");
    local prefix = "[" .. datetime .. "]";
    if (name and name ~= "") then prefix = prefix .. "[" .. name .. "]" end
    if (level and level ~= "") then prefix = prefix .. "[" .. level .. "]" end
    prefix = prefix .. "[" .. filepos .. "]";
    output(prefix, table.unpack(args));
end

local Logger = commonlib.inherit(nil, commonlib.gettable("commonlib.Logger"));

Logger.NONE = 0;
Logger.TRACE = 1;
Logger.DEBUG = 2;
Logger.INFO = 3;
Logger.WARN = 4;
Logger.ERROR = 5;

function Logger:ctor()
    self.m_level = self.INFO;
    self.m_output = print;
    self.m_depth = 4;
    self.m_name = "Logger";
end

function Logger:Init(level, output, depth)
    self.m_level = level or self.m_level;
    self.m_output = output or self.m_output;
    self.m_depth = depth or self.m_depth;
    return self;
end

function Logger:SetLevel(level) self.m_level = level; end

function Logger:SetOutput(output) self.m_output = output; end

function Logger:SetName(name) self.m_name = name; end

function Logger:Print(...) Print(self.m_output, self.m_name, self.m_depth, nil, ...); end

function Logger:Trace(...)
    if (self.m_level > self.TRACE) then return end
    Print(self.m_output, self.m_name, self.m_depth, "TRACE", debugstack(self.m_depth - 1), ...);
end

function Logger:Debug(...)
    if (self.m_level > self.DEBUG) then return end
    Print(self.m_output, self.m_name, self.m_depth, "DEBUG", ...);
end

function Logger:Info(...)
    if (self.m_level > self.INFO) then return end
    Print(self.m_output, self.m_name, self.m_depth, "INFO", ...);
end

function Logger:Warn(...)
    if (self.m_level > self.WARN) then return end
    Print(self.m_output, self.m_name, self.m_depth, "WARN", ...);
end

function Logger:Error(...)
    if (self.m_level > self.ERROR) then return end
    Print(self.m_output, self.m_name, self.m_depth, "ERROR", ...);
end

local log = Logger:new():Init(nil, nil, 5);

commonlib.print = function(...) log:Print(...) end
commonlib.trace = function(...) log:Trace(...) end
commonlib.debug = function(...) log:Debug(...) end
commonlib.info = function(...) log:Info(...) end
commonlib.warn = function(...) log:Warn(...) end
commonlib.error = function(...) log:Error(...) end
commonlib.echo = commonlib.print;
commonlib.log = log;
commonlib.debugstack = function(depth, message, is_print)
    local errmsg = debugstack(depth or 3, message);
    if (is_print == nil or is_print) then
        print(errmsg); 
    end
    return errmsg;
end

-- 保护执行
function commonlib.xpcall(func, ...)
    if (type(func) ~= "function") then return end
    return _G_xpcall(func, function(errmsg)
        return commonlib.debugstack(3, errmsg, false);
    end, ...);
end

-- 保护执行回调
function commonlib.xpcall_callback(func)
    return function(...)
        return commonlib.xpcall(func, ...);
    end
end

-- 加载字符串
function commonlib.loadstring(chunk, env, chunkname)
    if (type(chunk) ~= "string") then return nil, false end

    local func, errmsg = _G_load(chunk, chunkname or "dostring", "bt", env or _G);

    if (not func) then
        print("ERROR: loadstring->load " .. errmsg .. chunk);
    else
        return func, true;
    end

    return nil, false;
end

-- 执行字符串
function commonlib.dostring(chunk, env, chunkname)
    if (type(chunk) ~= "string") then return nil, false end
    local func, errmsg = _G_load(chunk, chunkname or "dostring", "bt", env or _G);
    if (not func) then
        commonlib.debugstack(2, errmsg);
    else
        local ok, result = _G_xpcall(func, function(xpcall_errmsg)
            return commonlib.debugstack(3, xpcall_errmsg, false);
        end);
        if (ok) then
            return result, true;
        end
    end

    return nil, false;
end

return Logger;
